package com.audaque.springboot.foshanupload.security.config;

import com.audaque.springboot.foshanupload.authcore.anno.AnonymousAccessAnno;
import com.audaque.springboot.foshanupload.authcore.properties.LoginProperties;
import com.audaque.springboot.foshanupload.core.component.ApplicationContextProvider;
import com.audaque.springboot.foshanupload.core.properties.StaticResourcePathProperties;
import com.audaque.springboot.foshanupload.core.properties.SwitchProperties;
import com.audaque.springboot.foshanupload.security.component.MyAuthenticationEntryPoint;
import com.audaque.springboot.foshanupload.security.filter.MyCaptchaFilter;
import com.audaque.springboot.foshanupload.security.filter.MyJwtAuthenticationFilter;
import com.audaque.springboot.foshanupload.security.filter.MyUsernamePasswordAuthenticationFilter;
import com.audaque.springboot.foshanupload.security.handler.MyAccessDeniedHandler;
import com.audaque.springboot.foshanupload.security.handler.MyLogoutSuccessHandler;
import com.audaque.springboot.foshanupload.security.handler.MyUrlAuthenticationFailureHandler;
import com.audaque.springboot.foshanupload.security.handler.MyUrlAuthenticationSuccessHandler;
import com.audaque.springboot.foshanupload.security.voter.MyExpressionVoter;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.ListUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.*;

/**
 * @author zgb
 * @desc ...
 * @date 2022-08-10 22:55:48
 */

@Configuration
@EnableWebSecurity
//启用spring方法级安全时，使用这个注解
//只有加了@EnableGlobalMethodSecurity(prePostEnabled=true) 那么在上面使用的 @PreAuthorize(“hasAuthority(‘admin’)”)才会生效
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private MyUrlAuthenticationFailureHandler myUrlAuthenticationFailureHandler;


    @Autowired
    private MyUrlAuthenticationSuccessHandler myAuthenticationSuccessHandler;


    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;


    @Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;


    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

/*

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
*/

    @Autowired
    private MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter;

    @Autowired
    private MyJwtAuthenticationFilter myJwtAuthenticationFilter;

    @Autowired
    private MyCaptchaFilter myCaptchaFilter;


    @Autowired
    private SwitchProperties switchProperties;


    @Autowired
    private LoginProperties loginProperties;

    @Autowired
    private StaticResourcePathProperties staticResourcePathProperties;

    @Autowired
    private ApplicationContextProvider applicationContextProvider;




    /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++==*/


    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++==*/


/*

    @Autowired
    private MySmsAuthenticationProvider mySmsAuthenticationProvider;

    @Autowired
    private MyMd5PasswordEncoder myMd5PasswordEncoder;


    @Autowired
    private MyUserDetailService myUserDetailService;

    @Autowired
    private MyUsernamePasswordAuthenticationProvider myUsernamePasswordAuthenticationProvider;

*/

/*

//认证方式2：测试不通过
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        //UsernamePasswordAuthenticationFilter配置
        //默认绑定/login
        //绑定用户详情服务和加密器
        //bCryptPasswordEncoder
        auth.userDetailsService(myUserDetailService).passwordEncoder(myMd5PasswordEncoder)
                .and()
                //添加自定义的认证管理类
                .authenticationProvider(mySmsAuthenticationProvider)
                .authenticationProvider(myUsernamePasswordAuthenticationProvider);

    }




*/



    /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++==*/


    /**
     * configure(WebSecurity web)不走过滤链的放行
     * 即不通过security 完全对外的/最大级别的放行
     **/

    /*用于影响全局安全性(配置资源，设置调试模式，通过实现自定义防火墙定义拒绝请求)的配置设置。
    一般用于配置全局的某些通用事物，例如静态资源等
    */
    // 直接放行了  如果是login接口 必须通过HttpSecurity 走过滤链 因为登录涉及 SecurityContextHolder
    @Override
    public void configure(WebSecurity web) throws Exception {

        List<String> list = staticResourcePathProperties.getList();
        String[] arr = list.toArray(new String[list.size()]);
        web.ignoring().antMatchers(arr);

        super.configure(web);
    }


    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //认证方式1
        httpSecurity
                //合并为一个config,把filter注入到容器，通过属性，用代码判断是否用addFilterBefore注入filter到security
                //自带的UsernamePasswordAuthenticationFilter匹配("/login", "POST"))
                //注入自定义过滤器
                //当我们调用 HttpSecurity#addFilterAt(A, B.class) 方法时（其中B一定是先于A添加，或者B本身就是默认的过滤器），他会将我们的添加的过滤器A在 FilterComparator ，并给给我们一个和B相同的序号（addFilterBefore(A, B.class) 给A的序号比B小1，addFilterAfter(A, B.class) 给A的序号比B大1）
                .addFilterBefore(myUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(myJwtAuthenticationFilter, MyUsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(myCaptchaFilter, MyJwtAuthenticationFilter.class)
        ;
        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/




   /*
   首先，HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 "admin"  ，密码是“ admin”，则将字符串"admin:admin" 使用Base64编码算法加密。加密结果可能是：YWtaW46YWRtaW4=。
然后，在Http请求中使用Authorization作为一个Header，“Basic YWtaW46YWRtaW4=“作为Header的值，发送给服务端。（注意这里使用Basic+空格+加密串）
服务器在收到这样的请求时，到达BasicAuthenticationFilter过滤器，将提取“ Authorization”的Header值，并使用用于验证用户身份的相同算法Base64进行解码。
解码结果与登录验证的用户名密码匹配，匹配成功则可以继续过滤器后续的访问。
所以，HttpBasic模式真的是非常简单又简陋的验证模式，Base64的加密算法是可逆的
不要开启，否则登录后跳到根目录
        httpSecurity
                //访问根目录弹出表单
                .httpBasic()
                                //鉴证失败处理器
                .authenticationEntryPoint(myAuthenticationEntryPoint);
              */
        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
        httpSecurity
                //表单认证
                .formLogin()
        //.defaultSuccessUrl("/sb")
        //登录页面
        //.loginPage("/authentication/loginPage")
        //绑定账号密码参数
        //.passwordParameter("username").passwordParameter("password")
        //登录处理接口，开放权限
        //.loginProcessingUrl(loginProperties.getLoginUrl())
        //.loginProcessingUrl("/login")
        //配置登录成功自定义处理类
        //.successHandler(myAuthenticationSuccessHandler)
        //配置登录失败自定义处理类
        //.failureHandler(myUrlAuthenticationFailureHandler)

        ;

        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

        httpSecurity

                //配置session
                .sessionManagement()
                //如果需要就在登录时创建一个session，正宗的security是要用cookie+session的，security维护session,开发者不用处理
                .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
        // 基于token，所以不需要session
        //.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        //.expiredUrl("/login‐view?error=EXPIRED_SESSION")                            //expired是指session过期了
        //.invalidSessionUrl("/login/indexPage?error=INVALID_SESSION")                            //invalidSession指传入的sessionId失效了。
        ;


        /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/


        httpSecurity

                //注入退出登录过滤器
                .logout()
                .logoutUrl(loginProperties.getLogoutUrl()) //默认退出地址
                //.logoutSuccessUrl("/login/indexPage?logout") //退出后的跳转地址
                //登出成功处理器
                .logoutSuccessHandler(myLogoutSuccessHandler)


                //.addLogoutHandler(logoutHandler) //添加一个LogoutHandler,用于实现用户退出时的清理工作.默认 SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler 。
                .invalidateHttpSession(true)//指定是否在退出时让HttpSession失效,默认是true
        ;


        /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

        /**
         * s:授权配置:
         */
        //设置UnauthorizedEntryPoint:不用认证的切点
        //只在这里配置静态资源和跨域放行
        List<String> list = staticResourcePathProperties.getList();
        String[] arr = list.toArray(new String[list.size()]);

        httpSecurity.authorizeRequests()
                .antMatchers(HttpMethod.GET,  //允许对于网站静态资源的无授权访问
                        arr
                )
                .permitAll()

                .antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求
                .permitAll()


                .requestMatchers(CorsUtils::isPreFlightRequest)
                .permitAll()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .antMatchers(loginProperties.getLoginUrl()).anonymous();
        // 所有加 AnonymousAccessAnno 注解的请求都允许匿名访问
        String[] anonymousUrls = this.getAnonymousUrls();
        if (anonymousUrls.length > 0) {
            httpSecurity.authorizeRequests().antMatchers(anonymousUrls).anonymous();
        }
        //其他所有请求需要登录, anyRequest 不能配置在 antMatchers 前面，一定要配置 .anyRequest().authenticated()，否则全部放行
        httpSecurity.authorizeRequests().anyRequest().authenticated();
        /**
         * e:授权配置
         */

        //鉴权器/投票器
        httpSecurity.authorizeRequests().accessDecisionManager(new UnanimousBased(Arrays.asList(
                new MyExpressionVoter(),
                new WebExpressionVoter(),
                new RoleVoter(),
                new AuthenticatedVoter())))
        ;
        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
        httpSecurity
                //注入异常处理过滤器
                .exceptionHandling()
                //鉴权失败处理器
                .accessDeniedHandler(myAccessDeniedHandler)
                //鉴证失败处理器
                .authenticationEntryPoint(myAuthenticationEntryPoint)


        ;



        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

        httpSecurity
                // 禁用缓存
                .headers().cacheControl()
        ;


        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/

        // 开启跨域
        httpSecurity
                .cors();
        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
        // 开启允许iframe 嵌套
        httpSecurity.headers().frameOptions().disable();

        /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
        httpSecurity
                // 由于使用的是JWT，我们这里不需要csrf
                //配置跨站请求伪造保护
                .csrf()
                //cookie 可以限制::「使用方式」::。「配置：Secure / HttpOnly」
                //Secure
                //Secure属性指定浏览器只有在加密协议 HTTPS 下，才能将这个 Cookie 发送到服务器。另一方面，如果当前协议是 HTTP，浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关，不需要指定值。如果通信是 HTTPS 协议，该开关自动打开。
                //HttpOnly
                //HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到，主要是Document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到，只有浏览器发出 HTTP 请求时，才会带上该 Cookie。
                //设置httponly=false，实现不限制cookie
                //.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                //无cookie，关闭跨站请求伪造保护
                .disable()

        ;


    }


    /**
     * 获取标有注解 AnonymousAccess 的访问路径
     */
    private String[] getAnonymousUrls() {
        // 获取所有的 RequestMapping
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = applicationContextProvider.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
        Set<String> allAnonymousAccess = new HashSet<>();
        // 循环 RequestMapping
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
            HandlerMethod value = infoEntry.getValue();
            // 获取方法上 AnonymousAccess 类型的注解
            AnonymousAccessAnno methodAnnotation = value.getMethodAnnotation(AnonymousAccessAnno.class);
            // 如果方法上标注了 AnonymousAccess 注解，就获取该方法的访问全路径
            if (methodAnnotation != null) {
                allAnonymousAccess.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            }
        }
        return allAnonymousAccess.toArray(new String[0]);
    }

}
